<?php

require_once('model/VocabularyDAO.php');
require_once('model/ConceptDAO.php');

class GenericSparql {

  public $client;
  public $graph;
  public $graphClause;
  public $vocDAO;

  public function __construct($endpoint, $graph) {
    $this->client = new EasyRdf_Sparql_Client($endpoint);
    $this->vocDAO = new VocabularyDAO();
    $this->graph = $graph;

    // set graphClause so that it can be used by all queries
    if ($graph[0] == '?') // default endpoint; query any graph (and catch it in a variable)
      $this->graphClause = "GRAPH $graph";
    elseif ($graph)       // query a specific graph
      $this->graphClause = "GRAPH <$graph>";
    else                  // query the default graph
      $this->graphClause = "";
  }

  /**
   * Generates and makes a query into a external vocabulary for an exact
   * match for a particular concept.
   * @param type $val
   * @return string
   */
  private function getExternalLabel($val) {
    $exuri = $val->getUri();

    try {
      $exsparql = $this->vocDAO->guessVocabularyFromURI($exuri)->getSparql();
    } catch (Exception $e) {
      //throw new Exception($e->getMessage());
      return null;
    }

    return $exsparql->queryExternalLabel($exuri);
  }

  private function getPropertyParam($val, $lang, $voc, $prop = null) {
    $ret = array();
    $ret['label'] = $val->label($lang) ? $val->label($lang)->getValue() : null;
    $ret['concept_uri'] = $voc->getLocalName($val->getUri());
    $ret['vocab'] = $voc->getId();
    $ret['prop'] = ($prop ? $ret['prop'] = $prop : $val->getUri());
    $ret['lang'] = $lang;
    $ret['exvocab'] = $voc->getId();
    return $ret;
  }

  private function guessVocabID($uri) {
    $exvoc = $this->vocDAO->guessVocabularyFromURI($uri);
    $exvocab = $exvoc->getId();
    return $exvocab;
  }

  /**
   * A function to convert a easyRDF resource to a ConceptDAO for the template engine.
   * @param easyRdfResource $resource the easyRDF resource representation of a concept.
   * @param string $lang the ui language eg. 'fi'.
   * @param string $vocid vocabulary id of the current concept eg. 'yso'.
   * @return ConceptDAO
   */
  public function resourceToDAO($resource, $lang, $vocid) {
    $conceptDAO = new ConceptDAO();
    $label = $resource->label($lang) ? $resource->label($lang)->getValue() : null;
    $conceptDAO->initialize($label, $this->guessVocabID($resource->getUri()), $resource->getURI(), $lang);

    $members_array = array();
    $long_uris = $resource->propertyUris();

    foreach ($long_uris as &$prop) {
      if (EasyRdf_Namespace::shorten($prop)) // shortening property labels if possible
        $prop = EasyRdf_Namespace::shorten($prop);

      // Iterating through every literal and adding these to the data object.
      foreach ($resource->allLiterals($prop, $lang) as $val) {
        $conceptDAO->addProperty($prop, null, null, $val->getLang(), $val->getValue());
        if ($prop === 'skos:prefLabel') { // property case 'skos:prefLabel'
          foreach ($resource->allLiterals($prop) as $lit) {
            if ($lit->getValue() != $val->getValue())
              $conceptDAO->setForeignLabel($lit->getValue(), $lit->getLang());
          }
        }
      }

      // Iterating through every resource and adding these to the data object.
      foreach ($resource->allResources($prop) as $val2) {
        if ($val2->get('rdfs:label') && $val2->getLiteral('rdfs:label', $lang)) {
          $conceptDAO->setLabel($val->getLiteral('rdfs:label', $lang)->getValue());
        }
        if ($prop === 'skos:narrower' && $val2->hasProperty('skos:member')) { // if collection group has been found  
          $members_array = array_merge($this->getGroupMembers($val2, $lang, $vocid, $prop, $resource), $members_array);
          // $conceptDAO->mergeNarrowerProperties($members_array);
          continue;
        }
        if ($prop === 'skos:exactMatch' || $prop === 'skos:narrowMatch' || $prop === 'skos:broadMatch' || $prop === 'owl:sameAs') {
          try {
            $propResult = $this->getExternalLabel($val2);
          } catch (Exception $e) {
            error_log($e->getMessage());
            continue;
          }
          if ($propResult)
            foreach ($propResult as $row) { // for choosing the UI-language label or if not found, the first language for external label
              if ($row->exlabel->getLang() == $lang || !(isset($label))) {
                $label = $row->exlabel->getValue();
                $label_lang = $row->exlabel->getLang();
                $path = explode('/', $val2->getUri());
                $exvocab = $path[sizeof($path) - 2];
                if ($row->exlabel->getLang() == $lang)
                  break;
              }
            }
          if (!isset($label_lang) || (!isset($label)))
            break;
        }
        $properties = $this->getPropertyParam($val2, $lang, $this->vocDAO->getVocabulary($vocid), $prop);
        if ($properties['label'] == null && $label != null && isset($label_lang)) {
          $properties['label'] = $label;
          $properties['lang'] = $label_lang;
          $properties['exvocab'] = $path[sizeof($path) - 2];
          // Here we check if exactmatches have improperly set external vocab id's and parse those straight from the uri.
        } if (!strstr($properties['concept_uri'], $properties['exvocab']) && strstr($properties['concept_uri'], 'http')) {
          $exvocab = explode('/', $properties['concept_uri']);
          $properties['exvocab'] = $exvocab[sizeof($exvocab) - 2];
        }
        $conceptDAO->setProperties($properties);
      }
    }
    // sorting the properties to a order preferred in the ONKI concept page.
    $conceptDAO->setSortedProperties($this->arbitrarySort($conceptDAO->getProperties()));

    if (!empty($members_array)) // if skos:narrower properties are actually groups we need to remove duplicates.
      $this->membersToDAOArray($members_array, $conceptDAO);

    return $conceptDAO;
  }

  public function membersToDAOArray($membersArray, $conceptDAO) {
    foreach ($membersArray as $topConcept) {
      $topDAO = $conceptDAO->addProperty('skos:narrower', $topConcept['parts'], $topConcept['vocab'], null, $topConcept['label'], $exvocab = null);
      foreach ($topConcept['sub_members'] as $subMember) {
        $topDAO->addSubMember('onki:sub', $subMember['label'], $subMember['parts'], $subMember['vocab'], null, $subMember['external']);
        $conceptDAO->deleteProperty('skos:narrower', $subMember['label']);
      }
    }
    $conceptDAO->sortProperties('skos:narrower');
  }

  public function getGroupMembers($rdfProperty, $lang, $vocid, $prop, $resource) {
    $members_array = Array();
    $prop_info = $this->getPropertyParam($rdfProperty, $lang, $this->vocDAO->getVocabulary($vocid));
    $external = false;
    if (strstr($prop_info['concept_uri'], 'http')) // for identifying concepts that are found with a uri not consistent with the current vocabulary
      $external = true;
    $members_array[$rdfProperty->getUri()] = array('type' => 'resource', 'label' => $prop_info['label'],
        'uri' => $prop_info['concept_uri'], 'vocab' => $prop_info['vocab'], 'parts' => $rdfProperty->getUri(), 'external' => $external);
    foreach ($rdfProperty->allResources('skos:member') as $member) {
      $arska = $resource->allResources($prop);
      foreach ($arska as $tulos) {
        if ($tulos->getUri() === $member->getUri()) {
          $prop_info = $this->getPropertyParam($tulos, $lang, $this->vocDAO->getVocabulary($vocid));
          $external = false;
          if (strstr($prop_info['concept_uri'], 'http')) // for identifying concepts that are found with a uri not consistent with the current vocabulary
            $external = true;
          if ($prop_info['label'] == null) { // fixes json encoded unicode characters causing labels to disappear in afo
            $prop_info['label'] = ('"' . $prop_info['concept_uri'] . '"');
            $prop_info['label'] = json_decode($prop_info['label']);
            $prop_info['concept_uri'] = $prop_info['label'];
            $prop_info['label'] = strtr($prop_info['label'], '_', ' ');
          }
          $members_array[$rdfProperty->getUri()]['sub_members'][] = array('type' => 'resource', 'label' => $prop_info['label'],
              'uri' => $prop_info['concept_uri'], 'vocab' => $prop_info['vocab'], 'parts' => $tulos->getUri(), 'external' => $external);
        }
      }
    }
    return $members_array;
  }

  /**
   * A fix for broken uris which direct to a another vocabulary even though
   * the concept is located inside the current vocabulary.
   * @param string $uri concept uri.
   * @param string $lang concept language.
   * @return string
   */
  private function fixLocalUri($uri, $lang) {
    $exvoc = $this->vocDAO->guessVocabularyFromURI($uri);
    $exvocab = $exvoc->getId();
    return $exvocab . '/' . $lang . '/page/' . $exvoc->getLocalName($uri);
  }

  /**
   * Sorting the result list to a arbitrary order defined below in mycompare()
   * @param unknown_type $sortable
   */
  public function arbitrarySort($sortable) {
    // sorting the result list to a arbitrary order defined below in mycompare()
    if ($sortable == null)
      return;
    uksort($sortable, array($this, 'mycompare'));
    foreach ($sortable as &$property_list)
      $property_list = $this->array_sorter($property_list, 'label');

    return $sortable;
  }

  /**
   * Compares the given objects and returns -1 or 1 depending which ought to be first.
   * $order defines the priorities of the different properties possible in the array.
   * @param string $a the first item to be compared
   * @param string $b the second item to be compared
   */
  public static function mycompare($a, $b) {
    if ($a == $b) {
      return 0;
    }
    $order = array("skos:note", "skosext:broaderGeneric", "skos:broader", "skosext:narrowerGeneric", "skos:narrower", "skos:related", "skos:altLabel");
    $position = array_search($a, $order);
    $position2 = array_search($b, $order);

    //if both are in the $order, then sort according to their order in $order...
    if ($position2 !== false && $position !== false) {
      return ($position < $position2) ? -1 : 1;
    }
    //if only one is in $order, then sort to put the one in $order first...
    if ($position !== false) {
      return -1;
    }
    if ($position2 !== false) {
      return 1;
    }

    //if neither in $order, then a simple alphabetic sort...
    return ($a < $b) ? -1 : 1;
  }

  /**
   * Used for counting number of concepts in a vocabulary.
   * @return int number of concepts in this vocabulary
   */
  public function countConcepts() {
    $gc = $this->graphClause;
    $query = <<<EOQ
      SELECT (COUNT(?conc) as ?c) WHERE {
        $gc {
          { ?conc a skos:Concept }
        }
      }
EOQ;
    $result = $this->client->query($query);
    foreach ($result as $row) {
      return $row->c->getValue();
    }
  }

  /**
   * Sorting function for sorting arrays by a named subarray value..
   * for example ['label'] in arbitrary sort.
   * @param array $array
   * @param type $on
   * @param enum $order PHP Sort options attribute eg. SORT_DESC
   * @return array which should now be sorted.
   */
  public function array_sorter($array, $on, $order = SORT_ASC) {
    $new_array = array();
    $sortable_array = array();

    if (count($array) > 0) {
      foreach ($array as $k => $v) {
        if (is_array($v)) {
          foreach ($v as $k2 => $v2) {
            if ($k2 == $on) {
              $sortable_array[$k] = strtolower($v2);
            }
          }
        } else {
          $sortable_array[$k] = $v;
        }
      }

      switch ($order) {
        case SORT_ASC:
          asort($sortable_array);
          break;
        case SORT_DESC:
          arsort($sortable_array);
          break;
      }

      foreach ($sortable_array as $k => $v) {
        $new_array[$k] = $array[$k];
      }
    }

    return $new_array;
  }

  /**
   * Counts the number of concepts in a easyRDF graph with a specific language.
   * @return Array containing count of concepts for each language and property.
   */
  public function countLangConcepts() {
    $gc = $this->graphClause;
    $ret = array();

    $TERMPROPS = array('skos:prefLabel', 'skos:altLabel', 'skos:hiddenLabel');
    foreach ($TERMPROPS as $prop) {
      $query = <<<EOQ
        SELECT
          (COUNT(?lfi) as ?fi)
          (COUNT(?lsv) as ?sv)
          (COUNT(?len) as ?en)
        WHERE {
          $gc {
            ?conc a skos:Concept .
            OPTIONAL {
              ?conc $prop ?lfi .
              FILTER (langMatches(lang(?lfi), 'fi'))
            }
            OPTIONAL {
              ?conc $prop ?lsv .
              FILTER (langMatches(lang(?lsv), 'sv'))
            }
            OPTIONAL {
              ?conc $prop ?len .
              FILTER (langMatches(lang(?len), 'en'))
            }
          }
        }
EOQ;
      // Count the number of terms in each language
      $result = $this->client->query($query);
      foreach ($result as $row) {
        $ret[$prop]['fi'] = $row->fi->getValue();
        $ret[$prop]['sv'] = $row->sv->getValue();
        $ret[$prop]['en'] = $row->en->getValue();
      }
    }
    return $ret;
  }

  /**
   * Returns information (as a graph) for one or more concept URIs
   * @param mixed $uris concept URI (string) or array of URIs
   * @param bool $globalCollections whether to use global skos:Collections (true) or not (false)
   * @return EasyRDF_Graph query result graph
   */
  public function queryConceptInfo($uris, $globalCollections = false, $lang = null, $vocid = null, $rest = false) {
    $gc = $this->graphClause;

    // if just a single URI is given, put it in an array regardless
    if (!is_array($uris))
      $uris = array($uris);

    // hacks for YSA etc.
    if ($globalCollections) {
      $optional_where = " OPTIONAL { ?x skos:member ?uri .
      ?x skos:prefLabel ?xl .
    }";
      $optional_member = "";
    } else {
      $optional_where = "";
      $optional_member = "OPTIONAL {
      ?x skos:member ?n .
      ?x skos:prefLabel ?xl . }";
    }

    $binds = "";
    foreach ($uris as $uri) {
      $binds .= <<<EOB
        BIND ( <$uri> as ?uri )
          ?uri ?p ?o .
          OPTIONAL { ?uri rdf:type ?type .
          ?type rdfs:label ?typelabel . }
          OPTIONAL { ?o rdf:type ?ot . }
          OPTIONAL { ?o skos:prefLabel ?ol . }
          OPTIONAL { ?n skos:broader ?uri .
          ?n rdf:type ?nt .
          ?n skos:prefLabel ?nl . }
          OPTIONAL { ?n skos:broader ?uri .
                     ?n rdf:type ?nt .
                     ?n skos:prefLabel ?nl . $optional_member
                   } 
          $optional_where
        } UNION {
EOB;
    }

    $construct = ($globalCollections) ? "?uri onki:memberOf ?x .
      ?x skos:prefLabel ?xl  " : "?uri skos:narrower ?x .
        ?x skos:member ?n .
        ?x skos:prefLabel ?xl .";

    $query = <<<EOQ
      CONSTRUCT {
       ?uri ?p ?o .
       ?o rdf:type ?ot .
       ?o skos:prefLabel ?ol .
       ?uri skos:narrower ?n .
       ?n rdf:type ?nt .
       ?n skos:prefLabel ?nl .
       ?type rdfs:label ?typelabel .
       $construct  
      } WHERE {
       $gc { {
        $binds }
        }
}
EOQ;
    $result = $this->client->query($query);

    if ($rest)
      return $result;

    if ($result->isEmpty())
      return;

    $conceptArray = array();
    foreach ($uris as $uri) {
      $conc = $result->resource($uri);
      $path = explode('/', $uri);
      $vocid = $path[sizeof($path) - 2];
      $conceptArray[$uri] = $this->resourceToDAO($conc, $lang, $vocid);
    }

    return $conceptArray;
  }

  /**
   * Retrieves conceptScheme information from the endpoint.
   * @param string $cs concept scheme URI
   * @return EasyRDF_Graph query result graph
   */
  public function queryConceptScheme($cs) {
    $gc = $this->graphClause;
    $query = <<<EOQ
      CONSTRUCT {
        <$cs> skos:prefLabel ?prefLabel .
        <$cs> dc:title ?title .
        <$cs> dc:description ?desc .
        <$cs> dc:date ?date .
        <$cs> owl:versionInfo ?versionInfo .
      } WHERE {
        $gc {
          { <$cs> skos:prefLabel ?preflabel }
          UNION
          { <$cs> dc:title ?title } UNION { <$cs> dc11:title ?title }
          UNION
          { <$cs> dc:description ?desc } UNION { <$cs> dc11:description ?desc }
          UNION
          { <$cs> dc:date ?date } UNION { <$cs> dc11:date ?date }
          UNION
          { <$cs> owl:versionInfo ?versionInfo }
        }
      }
EOQ;
    return $this->client->query($query);
  }

  /**
   * return a list of skos:ConceptScheme instances in the given graph
   * @return EasyRDF_Sparql_Result query result object
   */
  public function listConceptSchemes() {
    $gc = $this->graphClause;
    $query = "SELECT ?cs WHERE { $gc { ?cs a skos:ConceptScheme } } ORDER BY ?cs";
    return $this->client->query($query);
  }

  /**
   * a query for retrieving external labels for exactMatch and such.
   * @param string $exuri concept URI for which to query external labels
   * @return EasyRDF_Sparql_Result query result object
   */
  public function queryExternalLabel($exuri) {
    $gc = $this->graphClause;
    $query = <<<EOQ
          SELECT ?exlabel WHERE {
            $gc {
            <$exuri> skos:prefLabel ?exlabel .}}
EOQ;
    return $this->client->query($query);
  }

  /**
   * Query for concepts using a search term.
   * @param string $term search term
   * @param string $lang language code
   * @param int $limit maximum number of hits to retrieve; 0 for unlimited
   * @param int $offset offset of results to retrieve; 0 for beginning of list
   * @return EasyRDF_Sparql_Result query result object
   */
  public function queryConcepts($term, $lang, $limit, $offset) {
    $gc = $this->graphClause;
    $gr = $this->graph[0] == '?' ? $this->graph : '';
    $limit = ($limit) ? 'LIMIT ' . $limit : '';
    $offset = ($offset) ? 'OFFSET ' . $offset : '';

    // extra conditions for label language, if specified
    $labelcond = ($lang) ? "&& langMatches(lang(?label), '$lang')" : "";
    $labelcond2 = ($lang) ? "&& langMatches(lang(?alabel), '$lang')" : "";

    $orderextra = ($this->graph[0] == '?') ? $this->graph : '';

    $textmatch = "";
    if ($term == '0-9') {
      $term = '[0-9]';
    } elseif ($term == '!*') {
      $term = '[^A-Za-zåÅöÖäÄ0-9]';
    } elseif (strpos($term, '*') !== false || strpos($term, ' ') !== false) {
      while ($term[strlen($term) - 1] == '*' || $term[strlen($term) - 1] == ' ') {  // removes asterisks from the end of the search term
        $term = substr($term, 0, strlen($term) - 1);
        if (strlen($term) == 0)
          break;
      }
      while (strpos($term, '**') !== false)
        $term = str_replace('**', '*', $term); // removes futile asterisks
      $term = str_replace('*', '.*', $term); // just to fix regex-syntax to work as expected
    }

    $query = <<<EOQ
      SELECT DISTINCT ?s ?label ?alabel $gr WHERE {
       $gc {
        {
         ?s skos:prefLabel ?label .
         BIND (?label as ?match)
         ?s rdf:type skos:Concept .
         FILTER (regex(str(?label), "^$term.*", "i")
                 $labelcond)
        }
        UNION
        {
         ?s skos:altLabel ?alabel .
         BIND (?alabel as ?match)
         ?s rdf:type skos:Concept .
         ?s skos:prefLabel ?label .
         FILTER (regex(str(?alabel), "^$term.*", "i")
                 && !regex(str(?label), "^$term.*", "i")
                 && langMatches(lang(?alabel), lang(?label))
                 $labelcond2
                )
        }
       }

      } ORDER BY lcase(?match) $orderextra
      $limit
      $offset
EOQ;
    return $this->client->query($query);
  }

}
?>
